/*! 
 * jQuery Bootgrid v1.3.1 - 09/11/2015
 * Copyright (c) 2014-2015 Rafael Staib (http://www.jquery-bootgrid.com)
 * Licensed under MIT http://www.opensource.org/licenses/MIT
 */
;
(function($, window, undefined) {
	/* jshint validthis: true */
	"use strict";

	// GRID INTERNAL FIELDS
	// ====================

	var namespace = ".rs.jquery.bootgrid";

	// GRID INTERNAL FUNCTIONS
	// =====================

	function appendRow(row) {
		var that = this;

		function exists(item) {
			return that.identifier && item[that.identifier] === row[that.identifier];
		}

		if (!this.rows.contains(exists)) {
			this.rows.push(row);
			return true;
		}

		return false;
	}

	function findFooterAndHeaderItems(selector) {
		var footer = (this.footer) ? this.footer.find(selector) : $(), header = (this.header) ? this.header.find(selector) : $();
		return $.merge(footer, header);
	}

	function getParams(context) {
		return (context) ? $.extend({}, this.cachedParams, {
			ctx : context
		}) : this.cachedParams;
	}

	function getRequest() {
		var request = {
			current : this.current,
			rowCount : this.rowCount,
			sort : this.sortDictionary,
			searchPhrase : this.searchPhrase
		}, post = this.options.post;

		post = ($.isFunction(post)) ? post() : post;
		return this.options.requestHandler($.extend(true, request, post));
	}

	function getCssSelector(css) {
		return "." + $.trim(css).replace(/\s+/gm, ".");
	}

	function getUrl() {
		var url = this.options.url;
		return ($.isFunction(url)) ? url() : url;
	}

	function init() {
		this.element.trigger("initialize" + namespace);

		loadColumns.call(this); // Loads columns from HTML thead tag
		this.selection = this.options.selection && this.identifier != null;
		loadRows.call(this); // Loads rows from HTML tbody tag if ajax is
		// false
		prepareTable.call(this);
		renderTableHeader.call(this);
//		renderSearchField.call(this);
//		renderActions.call(this);
		loadData.call(this);

		this.element.trigger("initialized" + namespace);
	}

	function highlightAppendedRows(rows) {
		if (this.options.highlightRows) {
			// todo: implement
		}
	}

	function isVisible(column) {
		return column.visible;
	}

	function loadColumns() {
		var that = this, firstHeadRow = this.element.find("thead > tr").first(), sorted = false;

		/* jshint -W018 */
		firstHeadRow.children().each(function() {
			var $this = $(this), data = $this.data(), column = {
				id : data.columnId,
				identifier : that.identifier == null && data.identifier || false,
				converter : that.options.converters[data.converter || data.type] || that.options.converters["string"],
				text : $this.text(),
				align : data.align || "left",
				headerAlign : data.headerAlign || "left",
				cssClass : data.cssClass || "",
				headerCssClass : data.headerCssClass || "",
				formatter : that.options.formatters[data.formatter] || null,
				order : (!sorted && (data.order === "asc" || data.order === "desc")) ? data.order : null,
				searchable : !(data.searchable === false), // default: true
				sortable : data.sortable, // default: false
				visible : !(data.visible === false), // default: true
				visibleInSelection : !(data.visibleInSelection === false), // default:
				map : data.map==null ? { } : eval('(' + data.map + ')'),
				// true
				width : ($.isNumeric(data.width)) ? data.width + "px" : (typeof (data.width) === "string") ? data.width : null
			};
			that.columns.push(column);
			if (column.order != null) {
				that.sortDictionary[column.id] = column.order;
			}

			// Prevents multiple identifiers
			if (column.identifier) {
				that.identifier = column.id;
				that.converter = column.converter;
			}

			// ensures that only the first order will be applied in case of
			// multi sorting is disabled
			if (!that.options.multiSort && column.order !== null) {
				sorted = true;
			}
		});
		/* jshint +W018 */
	}

	/*
	 * response = { current: 1, rowCount: 10, rows: [{}, {}], sort: [{
	 * "columnId": "asc" }], total: 101 }
	 */

	function loadData() {
		var that = this;

		this.element._bgBusyAria(true).trigger("load" + namespace);
		showLoading.call(this);

		function containsPhrase(row) {
			var column, searchPattern = new RegExp(that.searchPhrase, (that.options.caseSensitive) ? "g" : "gi");

			for (var i = 0; i < that.columns.length; i++) {
				column = that.columns[i];
				if (column.searchable && column.visible && column.converter.to(column, row[column.id]).search(searchPattern) > -1) {
					return true;
				}
			}

			return false;
		}

		function update(rows, total) {
			that.currentRows = rows;
			setTotals.call(that, total);

			if (!that.options.keepSelection) {
				that.selectedRows = [];
			}

			renderRows.call(that, rows);
			renderInfos.call(that);
			renderPagination.call(that);

			that.element._bgBusyAria(false).trigger("loaded" + namespace);
		}

		if (this.options.ajax) {
			var request = getRequest.call(this), url = getUrl.call(this);

			if (url == null || typeof url !== "string" || url.length === 0) {
				throw new Error("Url setting must be a none empty string or a function that returns one.");
			}

			// aborts the previous ajax request if not already finished or
			// failed
			if (this.xqr) {
				this.xqr.abort();
			}

			var settings = {
				url : url,
				data : request,
				success : function(response) {
					that.xqr = null;

					if (typeof (response) === "string") {
						response = $.parseJSON(response);
					}

					response = that.options.responseHandler(response);

					that.current = response.current;
					update(response.rows, response.total);
				},
				error : function(jqXHR, textStatus, errorThrown) {
					that.xqr = null;

					if (textStatus !== "abort") {
						renderNoResultsRow.call(that); // overrides loading
						// mask
						that.element._bgBusyAria(false).trigger("loaded" + namespace);
					}
				}
			};
			settings = $.extend(this.options.ajaxSettings, settings);

			this.xqr = $.ajax(settings);
		} else {
			var rows = (this.searchPhrase.length > 0) ? this.rows.where(containsPhrase) : this.rows, total = rows.length;
			if (this.rowCount !== -1) {
				rows = rows.page(this.current, this.rowCount);
			}

			// todo: improve the following comment
			// setTimeout decouples the initialization so that adding event
			// handlers happens before
			window.setTimeout(function() {
				update(rows, total);
			}, 10);
		}
	}

	function loadRows() {
		if (!this.options.ajax) {
			var that = this, rows = this.element.find("tbody > tr");

			rows.each(function() {
				var $this = $(this), cells = $this.children("td"), row = {};

				$.each(that.columns, function(i, column) {
					row[column.id] = column.converter.from(column, cells.eq(i).text());
				});

				appendRow.call(that, row);
			});

			setTotals.call(this, this.rows.length);
			sortRows.call(this);
		}
	}

	function setTotals(total) {
		this.total = total;
		this.totalPages = (this.rowCount === -1) ? 1 : Math.ceil(this.total / this.rowCount);
	}

	function prepareTable() {
		var tpl = this.options.templates, wrapper = (this.element.parent().hasClass(this.options.css.responsiveTable)) ? this.element.parent() : this.element;

		this.element.addClass(this.options.css.table);

		// checks whether there is an tbody element; otherwise creates one
		if (this.element.children("tbody").length === 0) {
			this.element.append(tpl.body);
		}

		if (this.options.navigation & 1) {
			this.header = $(tpl.header.resolve(getParams.call(this, {
				id : this.element._bgId() + "-header"
			})));
			wrapper.before(this.header);
		}

		if (this.options.navigation & 2) {
			this.footer = $(tpl.footer.resolve(getParams.call(this, {
				id : this.element._bgId() + "-footer"
			})));
			wrapper.after(this.footer);
		}
	}

	function renderActions() {
		if (this.options.navigation !== 0) {
			var css = this.options.css, selector = getCssSelector(css.actions), actionItems = findFooterAndHeaderItems.call(this, selector);

			if (actionItems.length > 0) {
				var that = this, tpl = this.options.templates, actions = $(tpl.actions.resolve(getParams.call(this)));

				// Refresh Button
				if (this.options.ajax) {
					var refreshIcon = tpl.icon.resolve(getParams.call(this, {
						iconCss : css.iconRefresh
					})), refresh = $(tpl.actionButton.resolve(getParams.call(this, {
						content : refreshIcon,
						text : this.options.labels.refresh
					}))).on("click" + namespace, function(e) {
						// todo: prevent multiple fast clicks (fast click
						// detection)
						e.stopPropagation();
						that.current = 1;
						loadData.call(that);
					});
					actions.append(refresh);
				}

				// Row count selection
				renderRowCountSelection.call(this, actions);

				// Column selection
				renderColumnSelection.call(this, actions);

				replacePlaceHolder.call(this, actionItems, actions);
			}
		}
	}

	function renderColumnSelection(actions) {
		if (this.options.columnSelection && this.columns.length > 1) {
			var that = this, css = this.options.css, tpl = this.options.templates, icon = tpl.icon.resolve(getParams.call(this, {
				iconCss : css.iconColumns
			})), dropDown = $(tpl.actionDropDown.resolve(getParams.call(this, {
				content : icon
			}))), selector = getCssSelector(css.dropDownItem), checkboxSelector = getCssSelector(css.dropDownItemCheckbox), itemsSelector = getCssSelector(css.dropDownMenuItems);

			$.each(this.columns, function(i, column) {
				if (column.visibleInSelection) {
					var item = $(tpl.actionDropDownCheckboxItem.resolve(getParams.call(that, {
						name : column.id,
						label : column.text,
						checked : column.visible
					}))).on("click" + namespace, selector, function(e) {
						e.stopPropagation();

						var $this = $(this), checkbox = $this.find(checkboxSelector);
						if (!checkbox.prop("disabled")) {
							column.visible = checkbox.prop("checked");
							var enable = that.columns.where(isVisible).length > 1;
							$this.parents(itemsSelector).find(selector + ":has(" + checkboxSelector + ":checked)")._bgEnableAria(enable).find(checkboxSelector)._bgEnableField(enable);

							that.element.find("tbody").empty(); // Fixes an
							// column
							// visualization
							// bug
							renderTableHeader.call(that);
							loadData.call(that);
						}
					});
					dropDown.find(getCssSelector(css.dropDownMenuItems)).append(item);
				}
			});
			actions.append(dropDown);
		}
	}

	function renderInfos() {
		if (this.options.navigation !== 0) {
			var selector = getCssSelector(this.options.css.infos), infoItems = findFooterAndHeaderItems.call(this, selector);

			if (infoItems.length > 0) {
				var end = (this.current * this.rowCount), infos = $(this.options.templates.infos.resolve(getParams.call(this, {
					end : (this.total === 0 || end === -1 || end > this.total) ? this.total : end,
					start : (this.total === 0) ? 0 : (end - this.rowCount + 1),
					total : this.total
				})));

				replacePlaceHolder.call(this, infoItems, infos);
			}
		}
	}

	function renderNoResultsRow() {
		var tbody = this.element.children("tbody").first(), tpl = this.options.templates, count = this.columns.where(isVisible).length;

		if (this.selection) {
			count = count + 1;
		}
		tbody.html(tpl.noResults.resolve(getParams.call(this, {
			columns : count
		})));
	}

	function renderPagination() {
		if (this.options.navigation !== 0) {
			var selector = getCssSelector(this.options.css.pagination), paginationItems = findFooterAndHeaderItems.call(this, selector)._bgShowAria(this.rowCount !== -1);

			if (this.rowCount !== -1 && paginationItems.length > 0) {
				var tpl = this.options.templates, current = this.current, totalPages = this.totalPages, pagination = $(tpl.pagination.resolve(getParams.call(this))), offsetRight = totalPages
						- current, offsetLeft = (this.options.padding - current) * -1, startWith = ((offsetRight >= this.options.padding) ? Math.max(offsetLeft, 1) : Math.max((offsetLeft
						- this.options.padding + offsetRight), 1)), maxCount = this.options.padding * 2 + 1, count = (totalPages >= maxCount) ? maxCount : totalPages;

				renderPaginationItem.call(this, pagination, "first", "<i class=\"uk-icon-angle-double-left\"></i>", "first")._bgPaginationEnableAria(current > 1);
				renderPaginationItem.call(this, pagination, "prev", "<i class=\"uk-icon-angle-left\"></i>", "prev")._bgPaginationEnableAria(current > 1);

				for (var i = 0; i < count; i++) {
					var pos = i + startWith;
					renderPaginationItem.call(this, pagination, pos, pos, "page-" + pos)._bgPaginationEnableAria()._bgPaginationSelectAria(pos === current);
				}

				if (count === 0) {
					renderPaginationItem.call(this, pagination, 1, 1, "page-" + 1)._bgPaginationEnableAria(false)._bgPaginationSelectAria();
				}

				renderPaginationItem.call(this, pagination, "next", "<i class=\"uk-icon-angle-right\"></i>", "next")._bgPaginationEnableAria(totalPages > current);
				renderPaginationItem.call(this, pagination, "last", "<i class=\"uk-icon-angle-double-right\"></i>", "last")._bgPaginationEnableAria(totalPages > current);

				replacePlaceHolder.call(this, paginationItems, pagination);
			}
		}
	}

	function renderPaginationItem(list, page, text, markerCss) {
		var that = this, tpl = this.options.templates, css = this.options.css, values = getParams.call(this, {
			css : markerCss,
			text : text,
			page : page
		}), item = $(tpl.paginationItem.resolve(values)).on("click" + namespace, function(e) {
			e.stopPropagation();
			e.preventDefault();

			var $this = $(this), parent = $this.parent();
			if (!parent.hasClass("active") && !parent.hasClass("disabled")) {
				var commandList = {
					first : 1,
					prev : that.current - 1,
					next : that.current + 1,
					last : that.totalPages
				};
				var command = $this.data("page");
				that.current = commandList[command] || command;
				loadData.call(that);
			}
			$this.trigger("blur");
		});

		list.append(item);
		return item;
	}

	function renderRowCountSelection(actions) {
		var that = this, rowCountList = this.options.rowCount;

		function getText(value) {
			return (value === -1) ? that.options.labels.all : value;
		}

		if ($.isArray(rowCountList)) {
			var css = this.options.css, tpl = this.options.templates, dropDown = $(tpl.actionDropDown.resolve(getParams.call(this, {
				content : getText(this.rowCount)
			}))), menuSelector = getCssSelector(css.dropDownMenu), menuTextSelector = getCssSelector(css.dropDownMenuText), menuItemsSelector = getCssSelector(css.dropDownMenuItems), menuItemSelector = getCssSelector(css.dropDownItemButton);

			$.each(rowCountList, function(index, value) {
				var item = $(tpl.actionDropDownItem.resolve(getParams.call(that, {
					text : getText(value),
					action : value
				})))._bgSelectAria(value === that.rowCount).on("click" + namespace, menuItemSelector, function(e) {
					e.preventDefault();

					var $this = $(this), newRowCount = $this.data("action");
					if (newRowCount !== that.rowCount) {
						// todo: sophisticated solution needed for calculating
						// which page is selected
						that.current = 1; // that.rowCount === -1 ---> All
						that.rowCount = newRowCount;
						$this.parents(menuItemsSelector).children().each(function() {
							var $item = $(this), currentRowCount = $item.find(menuItemSelector).data("action");
							$item._bgSelectAria(currentRowCount === newRowCount);
						});
						$this.parents(menuSelector).find(menuTextSelector).text(getText(newRowCount));
						loadData.call(that);
					}
				});
				dropDown.find(menuItemsSelector).append(item);
			});
			actions.append(dropDown);
		}
	}

	function renderRows(rows) {
		if (rows.length > 0) {
			var that = this, css = this.options.css, tpl = this.options.templates, tbody = this.element.children("tbody").first(), allRowsSelected = true, html = "";
			
			$.each(rows, function(index, row) {
				var cells = "", rowAttr = " data-row-id=\"" + ((that.identifier == null) ? index : row[that.identifier]) + "\"", rowCss = "";
				
				if (that.selection) {
					var selected = ($.inArray(row[that.identifier], that.selectedRows) !== -1), selectBox = tpl.select.resolve(getParams.call(that, {
						type : "checkbox",
						value : row[that.identifier],
						checked : selected
					}));
					cells += tpl.cell.resolve(getParams.call(that, {
						content : selectBox,
						css : css.selectCell
					}));
					allRowsSelected = (allRowsSelected && selected);
					if (selected) {
						rowCss += css.selected;
						rowAttr += " aria-selected=\"true\"";
					}
				}

				var status = row.status != null && that.options.statusMapping[row.status];
				if (status) {
					rowCss += status;
				}

				$.each(that.columns, function(j, column) {
					if (column.visible) {
						var value = ($.isFunction(column.formatter)) ? column.formatter.call(that, column, row) : column.converter.to(column, row[column.id]), cssClass = (column.cssClass.length > 0) ? " "
								+ column.cssClass : "";
						cells += tpl.cell.resolve(getParams.call(that, {
							content : (value == null || value === "") ? "&nbsp;" : value,
							css : ((column.align === "right") ? css.right : (column.align === "center") ? css.center : css.left) + cssClass,
							style : (column.width == null) ? "" : "width:" + column.width + ";"
						}));
					}
				});

				if (rowCss.length > 0) {
					rowAttr += " class=\"" + rowCss + "\"";
				}
				html += tpl.row.resolve(getParams.call(that, {
					attr : rowAttr,
					cells : cells
				}));
			});

			// sets or clears multi selectbox state
			that.element.find("thead " + getCssSelector(that.options.css.selectAllBox)).iCheck(allRowsSelected?'check':'uncheck');

			tbody.html(html);

			registerRowEvents.call(this, tbody);
		} else {
			renderNoResultsRow.call(this);
		}
	}

	function registerRowEvents(tbody) {
		altair_md.checkbox_radio();
		
		var that = this, selectBoxSelector = getCssSelector(this.options.css.selectBox);
		
		if (this.selection) {
			tbody.off("ifClicked" + namespace, selectBoxSelector).on("ifClicked" + namespace, selectBoxSelector, function(e) {
				e.stopPropagation();

				var $this = $(this), id = that.converter.from($this, $this.val());

				if ($this.closest("tr").hasClass(that.options.css.selected)) {
					that.deselect([ id ]);
				} else {
					that.select([ id ]);
				}
			});
		}

		tbody.off("click" + namespace, "> tr").on(
				"click" + namespace,
				"> tr",
				function(e) {
					e.stopPropagation();

					var $this = $(this), id = (that.identifier == null) ? $this.data("row-id") : that.converter.from($this, $this.data("row-id") + ""), row = (that.identifier == null) ? that.currentRows[id]
							: that.currentRows.first(function(item) {
								return item[that.identifier] === id;
							});
					
					if (that.selection && that.options.rowSelect) {
						if ($this.hasClass(that.options.css.selected)) {
							that.deselect([ id ]);
						} else {
							that.select([ id ]);
						}
					}

					that.element.trigger("click" + namespace, [ that.columns, row ]);
				});
	}

	function renderSearchField() {
		if (this.options.navigation !== 0) {
			var css = this.options.css, selector = getCssSelector(css.search), searchItems = findFooterAndHeaderItems.call(this, selector);

			if (searchItems.length > 0) {
				var that = this, tpl = this.options.templates, timer = null, // fast
				// keyup
				// detection
				currentValue = "", searchFieldSelector = getCssSelector(css.searchField), search = $(tpl.search.resolve(getParams.call(this))), searchField = (search.is(searchFieldSelector)) ? search
						: search.find(searchFieldSelector);

				searchField.on("keyup" + namespace, function(e) {
					e.stopPropagation();
					var newValue = $(this).val();
					if (currentValue !== newValue || (e.which === 13 && newValue !== "")) {
						currentValue = newValue;
						if (e.which === 13 || newValue.length === 0 || newValue.length >= that.options.searchSettings.characters) {
							window.clearTimeout(timer);
							timer = window.setTimeout(function() {
								executeSearch.call(that, newValue);
							}, that.options.searchSettings.delay);
						}
					}
				});

				replacePlaceHolder.call(this, searchItems, search);
			}
		}
	}

	function executeSearch(phrase) {
		if (this.searchPhrase !== phrase) {
			this.current = 1;
			this.searchPhrase = phrase;
			loadData.call(this);
		}
	}

	function renderTableHeader() {
		var that = this, headerRow = this.element.find("thead > tr"), css = this.options.css, tpl = this.options.templates, html = "", sorting = this.options.sorting;

		if (this.selection) {
			var selectAllBox = (this.options.multiSelect) ? tpl.selectAll.resolve(getParams.call(that, {
				type : "checkbox",
				value : "all"
			})) : "";
			html += tpl.rawHeaderCell.resolve(getParams.call(that, {
				content : selectAllBox,
				css : css.selectCell
			}));
		}

		$.each(this.columns, function(index, column) {
			if (column.visible) {
				var sortOrder = that.sortDictionary[column.id], iconCss = ((sorting && sortOrder && sortOrder === "asc") ? css.iconUp : (sorting && sortOrder && sortOrder === "desc") ? css.iconDown
						: ""), icon = tpl.icon.resolve(getParams.call(that, {
					iconCss : iconCss
				})), align = column.headerAlign, cssClass = (column.headerCssClass.length > 0) ? " " + column.headerCssClass : "";
				var columnSortable = sorting && column.sortable && css.sortable;
				if(columnSortable) {
					html += tpl.headerCell.resolve(getParams.call(that, {
						column : column,
						icon : icon,
						sortable : sorting && column.sortable && css.sortable || "",
						css : ((align === "right") ? css.right : (align === "center") ? css.center : css.left) + cssClass,
						style : (column.width == null) ? "" : "width:" + column.width + ";"
					}));
				} else {
					html += tpl.rawHeaderCell.resolve(getParams.call(that, {
						content : column.text,
						css : ((align === "right") ? css.right : (align === "center") ? css.center : css.left) + cssClass
					}));
				}
			}
		});

		headerRow.html(html);

		if (sorting) {
			var sortingSelector = getCssSelector(css.sortable);
			headerRow.off("click" + namespace, sortingSelector).on("click" + namespace, sortingSelector, function(e) {
				e.preventDefault();

				setTableHeaderSortDirection.call(that, $(this));
				sortRows.call(that);
				loadData.call(that);
			});
		}

		// todo: create a own function for that piece of code
		if (this.selection && this.options.multiSelect) {
			var selectAllBoxSelector = getCssSelector(css.selectAllBox);
			headerRow.off("ifClicked" + namespace, selectAllBoxSelector).on("ifClicked" + namespace, selectAllBoxSelector, function(e) {
				e.stopPropagation();

				if ($(this).prop("checked")) {
					that.deselect();
				} else {
					that.select();
				}
			});
		}
	}

	function setTableHeaderSortDirection(element) {
		var css = this.options.css, iconSelector = getCssSelector(css.icon), columnId = element.data("column-id") || element.parents("th").first().data("column-id"), sortOrder = this.sortDictionary[columnId], icon = element
				.find(iconSelector);

		if (!this.options.multiSort) {
			element.parents("tr").first().find(iconSelector).removeClass(css.iconDown + " " + css.iconUp);
			this.sortDictionary = {};
		}

		if (sortOrder && sortOrder === "asc") {
			this.sortDictionary[columnId] = "desc";
			icon.removeClass(css.iconUp).addClass(css.iconDown);
		} else if (sortOrder && sortOrder === "desc") {
			if (this.options.multiSort) {
				var newSort = {};
				for ( var key in this.sortDictionary) {
					if (key !== columnId) {
						newSort[key] = this.sortDictionary[key];
					}
				}
				this.sortDictionary = newSort;
				icon.removeClass(css.iconDown);
			} else {
				this.sortDictionary[columnId] = "asc";
				icon.removeClass(css.iconDown).addClass(css.iconUp);
			}
		} else {
			this.sortDictionary[columnId] = "asc";
			icon.addClass(css.iconUp);
		}
	}

	function replacePlaceHolder(placeholder, element) {
		placeholder.each(function(index, item) {
			// todo: check how append is implemented. Perhaps cloning here is
			// superfluous.
			$(item).before(element.clone(true)).remove();
		});
	}

	function showLoading() {
		var that = this;

		window
				.setTimeout(
						function() {
							if (that.element._bgAria("busy") === "true") {
								var tpl = that.options.templates, thead = that.element.children("thead").first(), tbody = that.element.children("tbody").first(), firstCell = tbody.find("tr > td")
										.first(), padding = (that.element.height() - thead.height()) - (firstCell.height() + 20), count = that.columns.where(isVisible).length;

								if (that.selection) {
									count = count + 1;
								}
								tbody.html(tpl.loading.resolve(getParams.call(that, {
									columns : count
								})));
								if (that.rowCount !== -1 && padding > 0) {
									tbody.find("tr > td").css("padding", "20px 0 " + padding + "px");
								}
							}
						}, 250);
	}

	function sortRows() {
		var sortArray = [];

		function sort(x, y, current) {
			current = current || 0;
			var next = current + 1, item = sortArray[current];

			function sortOrder(value) {
				return (item.order === "asc") ? value : value * -1;
			}

			return (x[item.id] > y[item.id]) ? sortOrder(1) : (x[item.id] < y[item.id]) ? sortOrder(-1) : (sortArray.length > next) ? sort(x, y, next) : 0;
		}

		if (!this.options.ajax) {
			var that = this;

			for ( var key in this.sortDictionary) {
				if (this.options.multiSort || sortArray.length === 0) {
					sortArray.push({
						id : key,
						order : this.sortDictionary[key]
					});
				}
			}

			if (sortArray.length > 0) {
				this.rows.sort(sort);
			}
		}
	}

	// GRID PUBLIC CLASS DEFINITION
	// ====================

	/**
	 * Represents the jQuery Bootgrid plugin.
	 * 
	 * @class Grid
	 * @constructor
	 * @param element
	 *            {Object} The corresponding DOM element.
	 * @param options
	 *            {Object} The options to override default settings.
	 * @chainable
	 */
	var Grid = function(element, options) {
		this.element = $(element);
		this.origin = this.element.clone();
		this.options = $.extend(true, {}, Grid.defaults, this.element.data(), options);
		// overrides rowCount explicitly because deep copy ($.extend) leads to
		// strange behaviour
		var rowCount = this.options.rowCount = this.element.data().rowCount || options.rowCount || this.options.rowCount;
		this.columns = [];
		this.current = 1;
		this.currentRows = [];
		this.identifier = null; // The first column ID that is marked as
		// identifier
		this.selection = false;
		this.converter = null; // The converter for the column that is marked
		// as identifier
		this.rowCount = ($.isArray(rowCount)) ? rowCount[0] : rowCount;
		this.rows = [];
		this.searchPhrase = "";
		this.selectedRows = [];
		this.sortDictionary = {};
		this.total = 0;
		this.totalPages = 0;
		this.cachedParams = {
			lbl : this.options.labels,
			css : this.options.css,
			ctx : {}
		};
		this.header = null;
		this.footer = null;
		this.xqr = null;

		// todo: implement cache
	};

	/**
	 * An object that represents the default settings.
	 * 
	 * @static
	 * @class defaults
	 * @for Grid
	 * @example // Global approach $.bootgrid.defaults.selection = true;
	 * @example // Initialization approach $("#bootgrid").bootgrid({ selection =
	 *          true });
	 */
	Grid.defaults = {
		navigation : 2, // it's a flag: 0 = none, 1 = top, 2 = bottom, 3 = both
		// (top and bottom)
		padding : 2, // page padding (pagination)
		columnSelection : false,
		rowCount : [ 20, 25, 50, -1 ], // rows per page int or array of int (-1
		// represents "All")

		/**
		 * Enables row selection (to enable multi selection see also
		 * `multiSelect`). Default value is `false`.
		 * 
		 * @property selection
		 * @type Boolean
		 * @default false
		 * @for defaults
		 * @since 1.0.0
		 */
		selection : true,

		/**
		 * Enables multi selection (`selection` must be set to `true` as well).
		 * Default value is `false`.
		 * 
		 * @property multiSelect
		 * @type Boolean
		 * @default false
		 * @for defaults
		 * @since 1.0.0
		 */
		multiSelect : true,

		/**
		 * Enables entire row click selection (`selection` must be set to `true`
		 * as well). Default value is `false`.
		 * 
		 * @property rowSelect
		 * @type Boolean
		 * @default false
		 * @for defaults
		 * @since 1.1.0
		 */
		rowSelect : false,

		/**
		 * Defines whether the row selection is saved internally on filtering,
		 * paging and sorting (even if the selected rows are not visible).
		 * 
		 * @property keepSelection
		 * @type Boolean
		 * @default false
		 * @for defaults
		 * @since 1.1.0
		 */
		keepSelection : true,

		highlightRows : true, // highlights new rows (find the page of the
		// first new row)
		sorting : true,
		multiSort : false,

		/**
		 * General search settings to configure the search field behaviour.
		 * 
		 * @property searchSettings
		 * @type Object
		 * @for defaults
		 * @since 1.2.0
		 */
		searchSettings : {
			/**
			 * The time in milliseconds to wait before search gets executed.
			 * 
			 * @property delay
			 * @type Number
			 * @default 250
			 * @for searchSettings
			 */
			delay : 250,

			/**
			 * The characters to type before the search gets executed.
			 * 
			 * @property characters
			 * @type Number
			 * @default 1
			 * @for searchSettings
			 */
			characters : 1
		},

		/**
		 * Defines whether the data shall be loaded via an asynchronous HTTP
		 * (Ajax) request.
		 * 
		 * @property ajax
		 * @type Boolean
		 * @default false
		 * @for defaults
		 */
		ajax : true,

		/**
		 * Ajax request settings that shall be used for server-side
		 * communication. All setting except data, error, success and url can be
		 * overridden. For the full list of settings go to
		 * http://api.jquery.com/jQuery.ajax/.
		 * 
		 * @property ajaxSettings
		 * @type Object
		 * @for defaults
		 * @since 1.2.0
		 */
		ajaxSettings : {
			/**
			 * Specifies the HTTP method which shall be used when sending data
			 * to the server. Go to http://api.jquery.com/jQuery.ajax/ for more
			 * details. This setting is overriden for backward compatibility.
			 * 
			 * @property method
			 * @type String
			 * @default "POST"
			 * @for ajaxSettings
			 */
			method : "POST"
		},

		/**
		 * Enriches the request object with additional properties. Either a
		 * `PlainObject` or a `Function` that returns a `PlainObject` can be
		 * passed. Default value is `{}`.
		 * 
		 * @property post
		 * @type Object|Function
		 * @default function (request) { return request; }
		 * @for defaults
		 * @deprecated Use instead `requestHandler`
		 */
		post : {}, // or use function () { return {}; } (reserved properties
		// are "current", "rowCount", "sort" and "searchPhrase")

		/**
		 * Sets the data URL to a data service (e.g. a REST service). Either a
		 * `String` or a `Function` that returns a `String` can be passed.
		 * Default value is `""`.
		 * 
		 * @property url
		 * @type String|Function
		 * @default ""
		 * @for defaults
		 */
		url : "", // or use function () { return ""; }

		/**
		 * Defines whether the search is case sensitive or insensitive.
		 * 
		 * @property caseSensitive
		 * @type Boolean
		 * @default true
		 * @for defaults
		 * @since 1.1.0
		 */
		caseSensitive : true,

		// note: The following properties should not be used via data-api
		// attributes

		/**
		 * Transforms the JSON request object in what ever is needed on the
		 * server-side implementation.
		 * 
		 * @property requestHandler
		 * @type Function
		 * @default function (request) { return request; }
		 * @for defaults
		 * @since 1.1.0
		 */
		requestHandler : function(request) {
			request.page = request.current;
			request.limit = request.rowCount;

			var sort = "";
			for ( var key in request.sort) {
				sort = key + "," + request.sort[key];
			}
			request.sort = sort;

			return request;
		},

		/**
		 * Transforms the response object into the expected JSON response
		 * object.
		 * 
		 * @property responseHandler
		 * @type Function
		 * @default function (response) { return response; }
		 * @for defaults
		 * @since 1.1.0
		 */
		responseHandler : function(response) {
			return response;
		},

		/**
		 * A list of converters.
		 * 
		 * @property converters
		 * @type Object
		 * @for defaults
		 * @since 1.0.0
		 */
		converters : {
			numeric : {
				from : function(obj, value) {
					return +value;
				}, // converts from string to numeric
				to : function(obj, value) {
					return value + "";
				} // converts from numeric to string
			},
			string : {
				// default converter
				from : function(obj, value) {
					return value;
				},
				to : function(obj, value) {
					return value;
				}
			},
			bool : {
				from : function(obj, value) {
					return value;
				},
				to : function(obj, value) {
					return (value=="true"||value==true)?"是":"否";
				}
			},
			date: {
                from: function (obj, value) { return value; },
                to: function (obj, value) { return Grid.defaults.formatDate(value, "yyyy-MM-dd"); }
            },
            time: {
                from: function (obj, value) { return value; },
                to: function (obj, value) { return Grid.defaults.formatDate(value, "hh:mm:ss"); }
            },
            datetime: {
                from: function (obj, value) { return value; },
                to: function (obj, value) { return Grid.defaults.formatDate(value, "yyyy-MM-dd hh:mm:ss"); }
            },
            enums: {
                from: function (obj, value) { return value; },
                to: function (obj, value) {  return obj.map[value]||value; }
            }
		},

		formatDate : function(date, fmt) {
			if(!date) {
				return "";
			}
			
			date = new Date(date);
			var o = { 
				    "M+" : date.getMonth()+1,                 // 月份
				    "d+" : date.getDate(),                    // 日
				    "h+" : date.getHours(),                   // 小时
				    "m+" : date.getMinutes(),                 // 分
				    "s+" : date.getSeconds(),                 // 秒
				    "q+" : Math.floor((date.getMonth()+3)/3), // 季度
				    "S"  : date.getMilliseconds()             // 毫秒
				  }; 
				  if(/(y+)/.test(fmt)) 
				    fmt=fmt.replace(RegExp.$1, (date.getFullYear()+"").substr(4 - RegExp.$1.length)); 
				  for(var k in o) 
				    if(new RegExp("("+ k +")").test(fmt)) 
				  fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length))); 
				  return fmt; 
		},
		/**
		 * Contains all css classes.
		 * 
		 * @property css
		 * @type Object
		 * @for defaults
		 */
		css : {
			actions : "actions btn-group", // must be a unique class name or
			// constellation of class names
			// within the header and footer
			center : "text-center",
			columnHeaderAnchor : "column-header-anchor", // must be a unique
			// class name or
			// constellation of
			// class names
			// within the column
			// header cell
			columnHeaderText : "text",
			dropDownItem : "dropdown-item", // must be a unique class name or
			// constellation of class names
			// within the actionDropDown,
			dropDownItemButton : "dropdown-item-button", // must be a unique
			// class name or
			// constellation of
			// class names
			// within the
			// actionDropDown
			dropDownItemCheckbox : "dropdown-item-checkbox", // must be a
			// unique class
			// name or
			// constellation
			// of class
			// names within
			// the
			// actionDropDown
			dropDownMenu : "dropdown btn-group", // must be a unique class
			// name or constellation of
			// class names within the
			// actionDropDown
			dropDownMenuItems : "dropdown-menu pull-right", // must be a unique
			// class name or
			// constellation of
			// class names
			// within the
			// actionDropDown
			dropDownMenuText : "dropdown-text", // must be a unique class name
			// or constellation of class
			// names within the
			// actionDropDown
			footer : "bootgrid-footer container-fluid",
			header : "bootgrid-header container-fluid",
			icon : "icon glyphicon",
			iconColumns : "glyphicon-th-list",
			iconDown : "glyphicon-chevron-down",
			iconRefresh : "glyphicon-refresh",
			iconSearch : "glyphicon-search",
			iconUp : "glyphicon-chevron-up",
			infos : "infos", // must be a unique class name or constellation
			// of class names within the header and footer,
			left : "text-left",
			pagination : "pagination", // must be a unique class name or
			// constellation of class names within
			// the header and footer
			paginationButton : "button", // must be a unique class name or
			// constellation of class names
			// within the pagination

			/**
			 * CSS class to select the parent div which activates responsive
			 * mode.
			 * 
			 * @property responsiveTable
			 * @type String
			 * @default "table-responsive"
			 * @for css
			 * @since 1.1.0
			 */
			responsiveTable : "table-responsive",

			right : "text-right",
			search : "search form-group", // must be a unique class name or
			// constellation of class names
			// within the header and footer
			searchField : "search-field form-control",
			selectBox : "check_row", // must be a unique class name or
			selectAllBox : "check_all",
			// constellation of class names within
			// the entire table
			selectCell : "uk-text-center uk-table-middle small_col", // must be a unique class name or
			// constellation of class names within
			// the entire table

			/**
			 * CSS class to highlight selected rows.
			 * 
			 * @property selected
			 * @type String
			 * @default "active"
			 * @for css
			 * @since 1.1.0
			 */
			selected : "active",

			sortable : "sortable",
			table : "bootgrid-table table"
		},

		/**
		 * A dictionary of formatters.
		 * 
		 * @property formatters
		 * @type Object
		 * @for defaults
		 * @since 1.0.0
		 */
		formatters : {},

		/**
		 * Contains all labels.
		 * 
		 * @property labels
		 * @type Object
		 * @for defaults
		 */
		labels : {
			all : "全部",
			infos : "显示 {{ctx.start}} 到 {{ctx.end}}, 共 {{ctx.total}}",
			loading : "加载中...",
			noResults : "无数据",
			refresh : "刷新",
			search : "搜索"
		},

		/**
		 * Specifies the mapping between status and contextual classes to color
		 * rows.
		 * 
		 * @property statusMapping
		 * @type Object
		 * @for defaults
		 * @since 1.2.0
		 */
		statusMapping : {
			/**
			 * Specifies a successful or positive action.
			 * 
			 * @property 0
			 * @type String
			 * @for statusMapping
			 */
			0 : "success",

			/**
			 * Specifies a neutral informative change or action.
			 * 
			 * @property 1
			 * @type String
			 * @for statusMapping
			 */
			1 : "info",

			/**
			 * Specifies a warning that might need attention.
			 * 
			 * @property 2
			 * @type String
			 * @for statusMapping
			 */
			2 : "warning",

			/**
			 * Specifies a dangerous or potentially negative action.
			 * 
			 * @property 3
			 * @type String
			 * @for statusMapping
			 */
			3 : "danger"
		},

		/**
		 * Contains all templates.
		 * 
		 * @property templates
		 * @type Object
		 * @for defaults
		 */
		templates : {
			actionButton : "<button class=\"btn btn-default\" type=\"button\" title=\"{{ctx.text}}\">{{ctx.content}}</button>",
			actionDropDown : "<div class=\"{{css.dropDownMenu}}\"><button class=\"btn btn-default dropdown-toggle\" type=\"button\" data-toggle=\"dropdown\"><span class=\"{{css.dropDownMenuText}}\">{{ctx.content}}</span> <span class=\"caret\"></span></button><ul class=\"{{css.dropDownMenuItems}}\" role=\"menu\"></ul></div>",
			actionDropDownItem : "<li><a data-action=\"{{ctx.action}}\" class=\"{{css.dropDownItem}} {{css.dropDownItemButton}}\">{{ctx.text}}</a></li>",
			actionDropDownCheckboxItem : "<li><label class=\"{{css.dropDownItem}}\"><input name=\"{{ctx.name}}\" type=\"checkbox\" value=\"1\" class=\"{{css.dropDownItemCheckbox}}\" {{ctx.checked}} /> {{ctx.label}}</label></li>",
			actions : "<div class=\"{{css.actions}}\"></div>",
			body : "<tbody></tbody>",
			cell : "<td class=\"{{ctx.css}}\" style=\"{{ctx.style}}\">{{ctx.content}}</td>",
			footer:"<div id=\"{{ctx.id}}\" class=\"{{css.footer}}\"><p class=\"{{css.pagination}}\"></p></div>",
			footer_bak : "<div id=\"{{ctx.id}}\" class=\"{{css.footer}}\"><div class=\"row\"><div class=\"col-sm-6\"><p class=\"{{css.pagination}}\"></p></div><div class=\"col-sm-6 infoBar\"><p class=\"{{css.infos}}\"></p></div></div></div>",
			header : "<div id=\"{{ctx.id}}\" class=\"{{css.header}}\"><div class=\"row\"><div class=\"col-sm-12 actionBar\"><p class=\"{{css.search}}\"></p><p class=\"{{css.actions}}\"></p></div></div></div>",
			headerCell : "<th data-column-id=\"{{ctx.column.id}}\" class=\"{{ctx.css}}\" style=\"{{ctx.style}}\"><a href=\"javascript:void(0);\" class=\"{{css.columnHeaderAnchor}} {{ctx.sortable}}\"><span class=\"{{css.columnHeaderText}}\">{{ctx.column.text}}</span>{{ctx.icon}}</a></th>",
			icon : "<span class=\"{{css.icon}} {{ctx.iconCss}}\"></span>",
			infos : "<div class=\"{{css.infos}}\">{{lbl.infos}}</div>",
			loading : "<tr><td colspan=\"{{ctx.columns}}\" class=\"loading\">{{lbl.loading}}</td></tr>",
			noResults : "<tr><td colspan=\"{{ctx.columns}}\" class=\"no-results\">{{lbl.noResults}}</td></tr>",
			pagination : "<ul class=\"uk-pagination {{css.pagination}}\"></ul>",
			pagination_bak : "<div class=\"btn-group btn-group-sm {{css.pagination}}\"></div>",
			paginationItem : "<li data-page=\"{{ctx.page}}\"><a class=\"btn btn-default {{ctx.css}} {{css.paginationButton}}\">{{ctx.text}}</a></li>",
			rawHeaderCell : "<th class=\"{{ctx.css}}\">{{ctx.content}}</th>", // Used
			row : "<tr{{ctx.attr}}>{{ctx.cells}}</tr>",
			search : "<div class=\"{{css.search}}\"><div class=\"input-group\"><span class=\"{{css.icon}} input-group-addon {{css.iconSearch}}\"></span> <input type=\"text\" class=\"{{css.searchField}}\" placeholder=\"{{lbl.search}}\" /></div></div>",
			select : "<input name=\"select\" type=\"{{ctx.type}}\" data-md-icheck class=\"{{css.selectBox}}\" value=\"{{ctx.value}}\" {{ctx.checked}} />",
			selectAll : "<input name=\"select\" type=\"{{ctx.type}}\" data-md-icheck class=\"{{css.selectAllBox}}\" value=\"{{ctx.value}}\" {{ctx.checked}} />"
		}
	};

	/**
	 * Appends rows.
	 * 
	 * @method append
	 * @param rows
	 *            {Array} An array of rows to append
	 * @chainable
	 */
	Grid.prototype.append = function(rows) {
		if (this.options.ajax) {
			// todo: implement ajax PUT
		} else {
			var appendedRows = [];
			for (var i = 0; i < rows.length; i++) {
				if (appendRow.call(this, rows[i])) {
					appendedRows.push(rows[i]);
				}
			}
			sortRows.call(this);
			highlightAppendedRows.call(this, appendedRows);
			loadData.call(this);
			this.element.trigger("appended" + namespace, [ appendedRows ]);
		}

		return this;
	};

	/**
	 * Removes all rows.
	 * 
	 * @method clear
	 * @chainable
	 */
	Grid.prototype.clear = function() {
		if (this.options.ajax) {
			// todo: implement ajax POST
		} else {
			var removedRows = $.extend([], this.rows);
			this.rows = [];
			this.current = 1;
			this.total = 0;
			loadData.call(this);
			this.element.trigger("cleared" + namespace, [ removedRows ]);
		}

		return this;
	};

	/**
	 * Removes the control functionality completely and transforms the current
	 * state to the initial HTML structure.
	 * 
	 * @method destroy
	 * @chainable
	 */
	Grid.prototype.destroy = function() {
		// todo: this method has to be optimized (the complete initial state
		// must be restored)
		$(window).off(namespace);
		if (this.options.navigation & 1) {
			this.header.remove();
		}
		if (this.options.navigation & 2) {
			this.footer.remove();
		}
		this.element.before(this.origin).remove();

		return this;
	};

	/**
	 * Resets the state and reloads rows.
	 * 
	 * @method reload
	 * @chainable
	 */
	Grid.prototype.reload = function() {
		this.current = 1; // reset
		loadData.call(this);

		return this;
	};

	/**
	 * Removes rows by ids. Removes selected rows if no ids are provided.
	 * 
	 * @method remove
	 * @param [rowsIds]
	 *            {Array} An array of rows ids to remove
	 * @chainable
	 */
	Grid.prototype.remove = function(rowIds) {
		if (this.identifier != null) {
			var that = this;

			if (this.options.ajax) {
				// todo: implement ajax DELETE
			} else {
				rowIds = rowIds || this.selectedRows;
				var id, removedRows = [];

				for (var i = 0; i < rowIds.length; i++) {
					id = rowIds[i];

					for (var j = 0; j < this.rows.length; j++) {
						if (this.rows[j][this.identifier] === id) {
							removedRows.push(this.rows[j]);
							this.rows.splice(j, 1);
							break;
						}
					}
				}

				this.current = 1; // reset
				loadData.call(this);
				this.element.trigger("removed" + namespace, [ removedRows ]);
			}
		}

		return this;
	};

	/**
	 * Searches in all rows for a specific phrase (but only in visible cells).
	 * The search filter will be reseted, if no argument is provided.
	 * 
	 * @method search
	 * @param [phrase]
	 *            {String} The phrase to search for
	 * @chainable
	 */
	Grid.prototype.search = function(phrase) {
		phrase = phrase || "";

		if (this.searchPhrase !== phrase) {
			var selector = getCssSelector(this.options.css.searchField), searchFields = findFooterAndHeaderItems.call(this, selector);
			searchFields.val(phrase);
		}

		executeSearch.call(this, phrase);

		return this;
	};

	/**
	 * Selects rows by ids. Selects all visible rows if no ids are provided. In
	 * server-side scenarios only visible rows are selectable.
	 * 
	 * @method select
	 * @param [rowsIds]
	 *            {Array} An array of rows ids to select
	 * @chainable
	 */
	Grid.prototype.select = function(rowIds) {
		if (this.selection) {
			rowIds = rowIds || this.currentRows.propValues(this.identifier);
			
			var id, i, selectedRows = [];

			while (rowIds.length > 0 && !(!this.options.multiSelect && selectedRows.length === 1)) {
				id = rowIds.pop();
				if ($.inArray(id, this.selectedRows) === -1) {
					for (i = 0; i < this.currentRows.length; i++) {
						if (this.currentRows[i][this.identifier] === id) {
							selectedRows.push(this.currentRows[i]);
							this.selectedRows.push(id);
							break;
						}
					}
				}
			}

			if (selectedRows.length > 0) {
				var selectBoxSelector = getCssSelector(this.options.css.selectBox), selectMultiSelectBox = this.selectedRows.length >= this.currentRows.length;
				var selectAllBoxSelector = getCssSelector(this.options.css.selectAllBox)
				i = 0;
				while (!this.options.keepSelection && selectMultiSelectBox && i < this.currentRows.length) {
					selectMultiSelectBox = ($.inArray(this.currentRows[i++][this.identifier], this.selectedRows) !== -1);
				}
				this.element.find("thead " + selectAllBoxSelector).iCheck(selectMultiSelectBox?'check':'uncheck');

				if (!this.options.multiSelect) {
					this.element.find("tbody > tr " + selectBoxSelector + ":checked").trigger("ifClicked" + namespace);
				}

				for (i = 0; i < this.selectedRows.length; i++) {
					this.element.find("tbody > tr[data-row-id=\"" + this.selectedRows[i] + "\"]").addClass(this.options.css.selected)._bgAria("selected", "true").find(selectBoxSelector).iCheck('check');
				}

				this.element.trigger("selected" + namespace, [ selectedRows ]);
			}
		}

		return this;
	};

	/**
	 * Deselects rows by ids. Deselects all visible rows if no ids are provided.
	 * In server-side scenarios only visible rows are deselectable.
	 * 
	 * @method deselect
	 * @param [rowsIds]
	 *            {Array} An array of rows ids to deselect
	 * @chainable
	 */
	Grid.prototype.deselect = function(rowIds) {
		if (this.selection) {
			rowIds = rowIds || this.currentRows.propValues(this.identifier);
			
			var id, i, pos, deselectedRows = [];

			while (rowIds.length > 0) {
				id = rowIds.pop();
				pos = $.inArray(id, this.selectedRows);
				if (pos !== -1) {
					for (i = 0; i < this.currentRows.length; i++) {
						if (this.currentRows[i][this.identifier] === id) {
							deselectedRows.push(this.currentRows[i]);
							this.selectedRows.splice(pos, 1);
							break;
						}
					}
				}
			}

			if (deselectedRows.length > 0) {
				var selectAllBoxSelector = getCssSelector(this.options.css.selectAllBox);
				var selectBoxSelector = getCssSelector(this.options.css.selectBox);

				this.element.find("thead " + selectAllBoxSelector).iCheck('uncheck');
				for (i = 0; i < deselectedRows.length; i++) {
					this.element.find("tbody > tr[data-row-id=\"" + deselectedRows[i][this.identifier] + "\"]").removeClass(this.options.css.selected)._bgAria("selected", "false").find(
							selectBoxSelector).iCheck('uncheck');
				}
				
				this.element.trigger("deselected" + namespace, [ deselectedRows ]);
			}
		}

		return this;
	};

	/**
	 * Sorts the rows by a given sort descriptor dictionary. The sort filter
	 * will be reseted, if no argument is provided.
	 * 
	 * @method sort
	 * @param [dictionary]
	 *            {Object} A sort descriptor dictionary that contains the sort
	 *            information
	 * @chainable
	 */
	Grid.prototype.sort = function(dictionary) {
		var values = (dictionary) ? $.extend({}, dictionary) : {};

		if (values === this.sortDictionary) {
			return this;
		}

		this.sortDictionary = values;
		renderTableHeader.call(this);
		sortRows.call(this);
		loadData.call(this);

		return this;
	};

	/**
	 * Gets a list of the column settings. This method returns only for the
	 * first grid instance a value. Therefore be sure that only one grid
	 * instance is catched by your selector.
	 * 
	 * @method getColumnSettings
	 * @return {Array} Returns a list of the column settings.
	 * @since 1.2.0
	 */
	Grid.prototype.getColumnSettings = function() {
		return $.merge([], this.columns);
	};

	/**
	 * Gets the current page index. This method returns only for the first grid
	 * instance a value. Therefore be sure that only one grid instance is
	 * catched by your selector.
	 * 
	 * @method getCurrentPage
	 * @return {Number} Returns the current page index.
	 * @since 1.2.0
	 */
	Grid.prototype.getCurrentPage = function() {
		return this.current;
	};

	/**
	 * Gets the current rows. This method returns only for the first grid
	 * instance a value. Therefore be sure that only one grid instance is
	 * catched by your selector.
	 * 
	 * @method getCurrentPage
	 * @return {Array} Returns the current rows.
	 * @since 1.2.0
	 */
	Grid.prototype.getCurrentRows = function() {
		return $.merge([], this.currentRows);
	};

	/**
	 * Gets a number represents the row count per page. This method returns only
	 * for the first grid instance a value. Therefore be sure that only one grid
	 * instance is catched by your selector.
	 * 
	 * @method getRowCount
	 * @return {Number} Returns the row count per page.
	 * @since 1.2.0
	 */
	Grid.prototype.getRowCount = function() {
		return this.rowCount;
	};

	/**
	 * Gets the actual search phrase. This method returns only for the first
	 * grid instance a value. Therefore be sure that only one grid instance is
	 * catched by your selector.
	 * 
	 * @method getSearchPhrase
	 * @return {String} Returns the actual search phrase.
	 * @since 1.2.0
	 */
	Grid.prototype.getSearchPhrase = function() {
		return this.searchPhrase;
	};

	/**
	 * Gets the complete list of currently selected rows. This method returns
	 * only for the first grid instance a value. Therefore be sure that only one
	 * grid instance is catched by your selector.
	 * 
	 * @method getSelectedRows
	 * @return {Array} Returns all selected rows.
	 * @since 1.2.0
	 */
	Grid.prototype.getSelectedRows = function() {
		return $.merge([], this.selectedRows);
	};

	/**
	 * Gets the sort dictionary which represents the state of column sorting.
	 * This method returns only for the first grid instance a value. Therefore
	 * be sure that only one grid instance is catched by your selector.
	 * 
	 * @method getSortDictionary
	 * @return {Object} Returns the sort dictionary.
	 * @since 1.2.0
	 */
	Grid.prototype.getSortDictionary = function() {
		return $.extend({}, this.sortDictionary);
	};

	/**
	 * Gets a number represents the total page count. This method returns only
	 * for the first grid instance a value. Therefore be sure that only one grid
	 * instance is catched by your selector.
	 * 
	 * @method getTotalPageCount
	 * @return {Number} Returns the total page count.
	 * @since 1.2.0
	 */
	Grid.prototype.getTotalPageCount = function() {
		return this.totalPages;
	};

	/**
	 * Gets a number represents the total row count. This method returns only
	 * for the first grid instance a value. Therefore be sure that only one grid
	 * instance is catched by your selector.
	 * 
	 * @method getTotalRowCount
	 * @return {Number} Returns the total row count.
	 * @since 1.2.0
	 */
	Grid.prototype.getTotalRowCount = function() {
		return this.total;
	};

	// GRID COMMON TYPE EXTENSIONS
	// ============

	$.fn.extend({
		_bgAria : function(name, value) {
			return (value) ? this.attr("aria-" + name, value) : this.attr("aria-" + name);
		},

		_bgBusyAria : function(busy) {
			return (busy == null || busy) ? this._bgAria("busy", "true") : this._bgAria("busy", "false");
		},

		_bgRemoveAria : function(name) {
			return this.removeAttr("aria-" + name);
		},

		_bgEnableAria : function(enable) {
			return (enable == null || enable) ? this.removeClass("disabled")._bgAria("disabled", "false") : this.addClass("disabled")._bgAria("disabled", "true");
		},
		
		_bgPaginationEnableAria : function(enable) {
			return (enable == null || enable) ? this.removeClass("uk-disabled")._bgAria("disabled", "false") : this.addClass("uk-disabled")._bgAria("disabled", "true");
		},

		_bgEnableField : function(enable) {
			return (enable == null || enable) ? this.removeAttr("disabled") : this.attr("disabled", "disable");
		},

		_bgShowAria : function(show) {
			return (show == null || show) ? this.show()._bgAria("hidden", "false") : this.hide()._bgAria("hidden", "true");
		},

		_bgSelectAria : function(select) {
			return (select == null || select) ? this.addClass("active")._bgAria("selected", "true") : this.removeClass("active")._bgAria("selected", "false");
		},
		
		_bgPaginationSelectAria : function(select) {
			return (select == null || select) ? this.addClass("uk-active")._bgAria("selected", "true") : this.removeClass("uk-active")._bgAria("selected", "false");
		},

		_bgId : function(id) {
			return (id) ? this.attr("id", id) : this.attr("id");
		}
	});

	if (!String.prototype.resolve) {
		var formatter = {
			"checked" : function(value) {
				if (typeof value === "boolean") {
					return (value) ? "checked=\"checked\"" : "";
				}
				return value;
			}
		};

		String.prototype.resolve = function(substitutes, prefixes) {
			var result = this;
			$.each(substitutes, function(key, value) {
				if (value != null && typeof value !== "function") {
					if (typeof value === "object") {
						var keys = (prefixes) ? $.extend([], prefixes) : [];
						keys.push(key);
						result = result.resolve(value, keys) + "";
					} else {
						if (formatter && formatter[key] && typeof formatter[key] === "function") {
							value = formatter[key](value);
						}
						key = (prefixes) ? prefixes.join(".") + "." + key : key;
						var pattern = new RegExp("\\{\\{" + key + "\\}\\}", "gm");
						result = result.replace(pattern, (value.replace) ? value.replace(/\$/gi, "&#36;") : value);
					}
				}
			});
			return result;
		};
	}

	if (!Array.prototype.first) {
		Array.prototype.first = function(condition) {
			for (var i = 0; i < this.length; i++) {
				var item = this[i];
				if (condition(item)) {
					return item;
				}
			}
			return null;
		};
	}

	if (!Array.prototype.contains) {
		Array.prototype.contains = function(condition) {
			for (var i = 0; i < this.length; i++) {
				var item = this[i];
				if (condition(item)) {
					return true;
				}
			}
			return false;
		};
	}

	if (!Array.prototype.page) {
		Array.prototype.page = function(page, size) {
			var skip = (page - 1) * size, end = skip + size;
			return (this.length > skip) ? (this.length > end) ? this.slice(skip, end) : this.slice(skip) : [];
		};
	}

	if (!Array.prototype.where) {
		Array.prototype.where = function(condition) {
			var result = [];
			for (var i = 0; i < this.length; i++) {
				var item = this[i];
				if (condition(item)) {
					result.push(item);
				}
			}
			return result;
		};
	}

	if (!Array.prototype.propValues) {
		Array.prototype.propValues = function(propName) {
			var result = [];
			for (var i = 0; i < this.length; i++) {
				result.push(this[i][propName]);
			}
			return result;
		};
	}

	// GRID PLUGIN DEFINITION
	// =====================

	var old = $.fn.bootgrid;

	$.fn.bootgrid = function(option) {
		var args = Array.prototype.slice.call(arguments, 1), returnValue = null, elements = this.each(function(index) {
			var $this = $(this), instance = $this.data(namespace), options = typeof option === "object" && option;

			if (!instance && option === "destroy") {
				return;
			}
			if (!instance) {
				$this.data(namespace, (instance = new Grid(this, options)));
				init.call(instance);
			}
			if (typeof option === "string") {
				if (option.indexOf("get") === 0 && index === 0) {
					returnValue = instance[option].apply(instance, args);
				} else if (option.indexOf("get") !== 0) {
					return instance[option].apply(instance, args);
				}
			}
		});
		return (typeof option === "string" && option.indexOf("get") === 0) ? returnValue : elements;
	};

	$.fn.bootgrid.Constructor = Grid;

	// GRID NO CONFLICT
	// ===============

	$.fn.bootgrid.noConflict = function() {
		$.fn.bootgrid = old;
		return this;
	};

	// GRID DATA-API
	// ============

	$("[data-toggle=\"bootgrid\"]").bootgrid();
})(jQuery, window);